package com.zmc.lostfound.auth.service.impl;

import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zmc.lostfound.auth.dao.UserDao;
import com.zmc.lostfound.auth.entity.PermissionEntity;
import com.zmc.lostfound.auth.entity.RoleEntity;
import com.zmc.lostfound.auth.entity.UserEntity;
import com.zmc.lostfound.auth.entity.UserRoleEntity;
import com.zmc.lostfound.auth.service.PermissionService;
import com.zmc.lostfound.auth.service.RoleService;
import com.zmc.lostfound.auth.service.UserRoleService;
import com.zmc.lostfound.auth.service.UserService;
import com.zmc.lostfound.auth.vo.*;
import com.zmc.lostfound.common.constant.AuthConstant;
import com.zmc.lostfound.common.dto.RoleAndPermissionDTO;
import com.zmc.lostfound.common.dto.SimpleEmailDTO;
import com.zmc.lostfound.common.dto.SmsSendCaptchaDTO;
import com.zmc.lostfound.common.dto.post.FromUser;
import com.zmc.lostfound.common.exception.MyException;
import com.zmc.lostfound.common.feign.ThirdPartyFeign;
import com.zmc.lostfound.common.utils.*;
import com.zmc.lostfound.common.utils.redis.RedisCacheUtil;
import org.apache.http.HttpStatus;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.zmc.lostfound.common.constant.AuthConstant.DEFAULT_IMG_URL;
import static com.zmc.lostfound.common.constant.RabbitMqConstant.USER_EXCHANGE;
import static com.zmc.lostfound.common.constant.RabbitMqConstant.USER_SAVE_KEY;
import static com.zmc.lostfound.common.constant.RedisConstant.*;

/**
 * @author ZMC
 * @description 用户信息管理业务类
 * @email 2869581855@qq.com
 * @date 2023-07-05 15:17:37
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserDao, UserEntity> implements UserService {

    @Resource
    private UserService currentProxy;

    private final UserRoleService userRoleService;

    private final RoleService roleService;

    private final PermissionService permissionService;

    private final ThirdPartyFeign thirdPartyFeign;

    private final StringRedisTemplate stringRedisTemplate;

    private final RabbitTemplate rabbitTemplate;

    private final RedisCacheUtil redisCacheUtil;

    private final ThreadPoolExecutor threadPoolExecutor;

    public UserServiceImpl(UserRoleService userRoleService, RoleService roleService, PermissionService permissionService, ThirdPartyFeign thirdPartyFeign, StringRedisTemplate stringRedisTemplate, RabbitTemplate rabbitTemplate, RedisCacheUtil redisCacheUtil, ThreadPoolExecutor threadPoolExecutor) {
        this.userRoleService = userRoleService;
        this.roleService = roleService;
        this.permissionService = permissionService;
        this.thirdPartyFeign = thirdPartyFeign;
        this.stringRedisTemplate = stringRedisTemplate;
        this.rabbitTemplate = rabbitTemplate;
        this.redisCacheUtil = redisCacheUtil;
        this.threadPoolExecutor = threadPoolExecutor;
    }

    /**
     * 分页查询用户基本信息
     *
     * @param pageParams 分页查询参数
     * @return PageResult
     */
    @Override
    public PageResult queryPage(PageParams pageParams) {
        Integer pageNo = pageParams.getPageNo();
        Integer pageSize = pageParams.getPageSize();
        Page<UserEntity> page = new Page<>(pageNo, pageSize);
        QueryWrapper<UserEntity> queryWrapper = new QueryWrapper<>();
        Page<UserEntity> userPage = this.page(page, queryWrapper);
        List<UserEntity> userList = userPage.getRecords();
        if (CollectionUtil.isEmpty(userList)) {
            return new PageResult(userPage);
        }
        List<UserBaseInfoVO> collect = userList.stream()
                .map(userEntity -> BeanUtil.copyProperties(userEntity, UserBaseInfoVO.class))
                .collect(Collectors.toList());
        return new PageResult(collect, pageNo, pageSize, collect.size());
    }

    /**
     * 按用户ids批量查询用户基本信息
     *
     * @param userIds 用户ids
     * @return List<UserBaseInfoVO>
     */
    @Override
    public List<UserBaseInfoVO> queryBaseInfoByIds(List<Long> userIds) {
        if (CollectionUtil.isEmpty(userIds)) {
            MyException.throwException("查询失败: userIds为空");
        }
        List<UserEntity> userEntityList = this.listByIds(userIds);
        if (CollectionUtil.isEmpty(userEntityList)) {
            return null;
        }
        return userEntityList.stream()
                .map(userEntity -> BeanUtil.copyProperties(userEntity, UserBaseInfoVO.class))
                .collect(Collectors.toList());
    }

    /**
     * 按用户ids批量查询用户部分信息
     *
     * @param userIds 用户ids
     * @return ist<FromUser>
     */
    @Override
    public List<FromUser> queryPartInfoByIds(List<Long> userIds) {
        if (CollectionUtil.isEmpty(userIds)) {
            MyException.throwException("查询失败: userIds为空");
        }
        List<UserEntity> userEntityList = this.query()
                .select("id", "nickname", "img_url")
                .in("id", userIds)
                .list();
        if (CollectionUtil.isEmpty(userEntityList)) {
            return null;
        }
        return userEntityList.stream()
                .map(userEntity -> BeanUtil.copyProperties(userEntity, FromUser.class))
                .collect(Collectors.toList());
    }

    /**
     * 按id查询用户详情信息
     *
     * @param id 用户id
     * @return UserDetailInfoVO
     */
    @Override
    public UserDetailInfoVO queryDetailInfoById(Long id) {
        if (id == null || id <= 0) {
            MyException.throwException("查询失败: 用户id不能为null且必须大于0");
        }
        String key = CACHE_USER_BASE_KEY_PREFIX + id;
        UserDetailInfoVO userDetailInfoVO;
        // 查询缓存
        UserBaseInfoVO userBaseInfoVO = redisCacheUtil.simpleMapQuery(key, UserBaseInfoVO.class);
        // 缓存命中
        if (userBaseInfoVO != null && userBaseInfoVO.getId() != null) {
            userDetailInfoVO = BeanUtil.copyProperties(userBaseInfoVO, UserDetailInfoVO.class);
            // 查询用户的角色信息
            CompletableFuture<Void> roleFuture = CompletableFuture.runAsync(() -> userDetailInfoVO.setRoleList(roleService.queryInfoByUserId(id)), threadPoolExecutor);
            // 查询用户的权限信息
            CompletableFuture<Void> permissionFuture = CompletableFuture.runAsync(() -> userDetailInfoVO.setPermissionList(permissionService.queryInfoByUserId(id)), threadPoolExecutor);
            CompletableFuture.allOf(roleFuture, permissionFuture).join();
            return userDetailInfoVO;
        }
        // 缓存命中"空对象", 表示数据库中不存在
        if (userBaseInfoVO != null) {
            return null;
        }
        // 缓存未命中，数据库中可能存在
        userDetailInfoVO = this.baseMapper.queryDetailInfo(id, null);
        if (userDetailInfoVO == null) {
            // 利用缓存空对象的思想，避免缓存穿透
            Map<String, Object> userDetailInfoMap = new HashMap<>(1);
            userDetailInfoMap.put("id", null);
            redisCacheUtil.setMapWithTTL(key, userDetailInfoMap, CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 查询用户的角色信息
        CompletableFuture<Void> roleFuture = CompletableFuture.runAsync(() -> userDetailInfoVO.setRoleList(roleService.queryInfoByUserId(id)), threadPoolExecutor);
        // 查询用户的权限信息
        CompletableFuture<Void> permissionFuture = CompletableFuture.runAsync(() -> userDetailInfoVO.setPermissionList(permissionService.queryInfoByUserId(id)), threadPoolExecutor);
        // 添加用户基本信息到缓存，并设置随机TTL，避免缓存雪崩
        CompletableFuture.runAsync(() -> {
            Map<String, Object> userBaseInfoMap = BeanUtil.beanToMap(BeanUtil.copyProperties(userDetailInfoVO, UserBaseInfoVO.class), new HashMap<>(15),
                    CopyOptions.create()
                            .setIgnoreNullValue(true)
                            .setFieldValueEditor((fieldName, fieldValue) -> {
                                if (fieldValue == null) {
                                    return null;
                                }
                                return fieldValue.toString();
                            }));
            redisCacheUtil.setMapWithTTL(key, userBaseInfoMap, CACHE_USER_BASE_KEY_PREFIX_TTL, TimeUnit.SECONDS, true);
        }, threadPoolExecutor);
        CompletableFuture.allOf(roleFuture, permissionFuture).join();
        return userDetailInfoVO;
    }

    /**
     * 按id查询用户基本信息
     *
     * @param id 用户id
     * @return UserBaseInfoVO
     */
    @Override
    public UserBaseInfoVO queryBaseInfoById(Long id) {
        if (id == null || id <= 0) {
            MyException.throwException("查询失败: 用户id不能为null且必须大于0");
        }
        String key = CACHE_USER_BASE_KEY_PREFIX + id;
        // 查询缓存
        UserBaseInfoVO userBaseInfoVO = redisCacheUtil.simpleMapQuery(key, UserBaseInfoVO.class);
        // 缓存命中
        if (userBaseInfoVO != null && userBaseInfoVO.getId() != null) {
            return userBaseInfoVO;
        }
        // 缓存命中"空对象", 表示数据库中不存在
        if (userBaseInfoVO != null) {
            return null;
        }
        // 缓存未命中，数据库中可能存在
        UserEntity userEntity = this.getById(id);
        if (userEntity == null) {
            // 利用缓存空对象的思想，避免缓存穿透
            Map<String, Object> userBaseInfoMap = new HashMap<>(1);
            userBaseInfoMap.put("id", null);
            redisCacheUtil.setMapWithTTL(key, userBaseInfoMap, CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        userBaseInfoVO = BeanUtil.copyProperties(userEntity, UserBaseInfoVO.class);
        // 添加用户基本信息到缓存，并设置随机TTL，避免缓存雪崩
        UserBaseInfoVO finalUserBaseInfoVO = userBaseInfoVO;
        CompletableFuture.runAsync(() -> {
            Map<String, Object> userBaseInfoMap = BeanUtil.beanToMap(finalUserBaseInfoVO, new HashMap<>(15),
                    CopyOptions.create()
                            .setIgnoreNullValue(true)
                            .setFieldValueEditor((fieldName, fieldValue) -> {
                                if (fieldValue == null) {
                                    return null;
                                }
                                return fieldValue.toString();
                            }));
            redisCacheUtil.setMapWithTTL(key, userBaseInfoMap, CACHE_USER_BASE_KEY_PREFIX_TTL, TimeUnit.SECONDS, true);
        }, threadPoolExecutor);
        return userBaseInfoVO;
    }

    /**
     * 按id查询用户的角色与权限信息
     *
     * @param id 用户id
     * @return RoleAndPermissionDTO
     */
    @Override
    public RoleAndPermissionDTO queryRoleAndPermissionById(Long id) {
        if (id == null || id <= 0) {
            MyException.throwException("查询失败: 用户id不能为null且必须大于0");
        }
        return redisCacheUtil.queryWithPassThrough(CACHE_ROLE_PERMISSION_KEY_PREFIX, id, RoleAndPermissionDTO.class,
                this::queryRoleAndPermissionByIdWithMySql, CACHE_ROLE_PERMISSION_KEY_PREFIX_TTL, TimeUnit.SECONDS);
    }

    /**
     * 按id查询用户的角色与权限信息
     *
     * @param id 用户id
     * @return RoleAndPermissionDTO
     */
    private RoleAndPermissionDTO queryRoleAndPermissionByIdWithMySql(Long id) {
        RoleAndPermissionDTO roleAndPermissionDTO = new RoleAndPermissionDTO();
        // 查询用户的角色信息
        CompletableFuture<Void> roleFuture = CompletableFuture.runAsync(() -> {
            List<RoleEntity> roleList = roleService.queryInfoByUserId(id);
            if (CollectionUtil.isNotEmpty(roleList)) {
                roleAndPermissionDTO.setRoleCodeList(roleList.stream().map(RoleEntity::getRoleCode).collect(Collectors.toList()));
            }
        }, threadPoolExecutor);
        // 查询用户的权限信息
        CompletableFuture<Void> permissionFuture = CompletableFuture.runAsync(() -> {
            List<PermissionEntity> permissionList = permissionService.queryInfoByUserId(id);
            if (CollectionUtil.isNotEmpty(permissionList)) {
                roleAndPermissionDTO.setPermissionCodeList(permissionList.stream().map(PermissionEntity::getPermissionCode).collect(Collectors.toList()));
            }
        }, threadPoolExecutor);
        // 等待所有异步任务都执行完，再返回用户的角色与权限信息
        CompletableFuture.allOf(roleFuture, permissionFuture).join();
        return roleAndPermissionDTO;
    }

    /**
     * 按手机号查询用户详情信息
     *
     * @param phone 手机号
     * @return UserDetailInfoVO
     */
    @Override
    public UserDetailInfoVO queryDetailInfoByPhone(String phone) {
        if (RegexUtil.isPhoneInvalid(phone)) {
            MyException.throwException("查询失败: 手机号码不合法");
        }
        UserDetailInfoVO userDetailInfoVO = this.baseMapper.queryDetailInfo(null, phone);
        if (userDetailInfoVO == null) {
            return null;
        }
        // 查询用户的权限信息
        userDetailInfoVO.setPermissionList(permissionService.queryInfoByUserId(userDetailInfoVO.getId()));
        return userDetailInfoVO;
    }

    /**
     * 按角色id查询用户基本信息
     *
     * @param roleId 角色id
     * @return List<UserBaseInfoVO>
     */
    @Override
    public List<UserBaseInfoVO> queryBaseInfoByRoleId(Long roleId) {
        if (roleId == null || roleId <= 0) {
            MyException.throwException("查询失败: 角色id不能为null且必须大于0");
        }
        // 1.查询userIds
        List<UserRoleEntity> userRoleList = userRoleService.query().select("user_id").eq("role_id", roleId).list();
        if (CollectionUtil.isEmpty(userRoleList)) {
            return null;
        }
        List<Long> userIds = userRoleList.stream().map(UserRoleEntity::getUserId).collect(Collectors.toList());
        // 2.根据userIds批量查询用户基本信息
        List<UserEntity> userEntityList = this.listByIds(userIds);
        if (CollectionUtil.isEmpty(userEntityList)) {
            return null;
        }
        return userEntityList.stream()
                .map(userEntity -> BeanUtil.copyProperties(userEntity, UserBaseInfoVO.class))
                .collect(Collectors.toList());
    }

    /**
     * 管理员新增用户基本信息
     *
     * @param userSaveWithAdminVO 用户基本信息新增/修改请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result insertUserWithAdmin(UserSaveWithAdminVO userSaveWithAdminVO) {
        // 判断该手机号是否已绑定其它用户
        Integer count = this.query().eq("phone", userSaveWithAdminVO.getPhone()).count();
        if (count != null && count > 0) {
            return Result.error("新增用户信息失败: 该手机号已绑定其它用户，请更换手机号");
        }
        // 如果设置了QQ邮箱，判断该QQ邮箱是否已绑定其它用户
        String qqEmail = userSaveWithAdminVO.getQqEmail();
        if (qqEmail != null) {
            count = this.query().eq("qq_email", qqEmail).count();
            if (count != null && count > 0) {
                return Result.error("新增用户信息失败: 该QQ邮箱已绑定其它用户，请更换QQ邮箱");
            }
        }
        // 如果没有设置密码，设置随机密码并使用Md5盐值加密
        String password = userSaveWithAdminVO.getPassword();
        userSaveWithAdminVO.setPassword(password != null ? Md5Util.encryption(password) : Md5Util.encryption(RandomUtil.randomNumbers(10)));
        // 如果没有设置昵称，设置随机昵称
        if (StrUtil.isBlank(userSaveWithAdminVO.getNickname())) {
            userSaveWithAdminVO.setNickname("user_" + RandomUtil.randomNumbers(10));
        }
        // 如果没有设置头像，设置默认头像
        if (StrUtil.isBlank(userSaveWithAdminVO.getImgUrl())) {
            userSaveWithAdminVO.setImgUrl(DEFAULT_IMG_URL);
        }
        UserEntity userEntity = BeanUtil.copyProperties(userSaveWithAdminVO, UserEntity.class);
        if (!this.save(userEntity)) {
            MyException.throwException("新增用户基本信息失败");
        }
        // 发送消息到user.topic交换机，实现MySQL与ES的数据同步
        CompletableFuture.runAsync(() -> rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, userEntity.getId()), threadPoolExecutor);
        return Result.ok("新增成功");
    }

    /**
     * 管理员修改用户基本信息
     *
     * @param userSaveWithAdminVO 用户基本信息新增/修改请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result updateUserWithAdmin(UserSaveWithAdminVO userSaveWithAdminVO) {
        Long id = userSaveWithAdminVO.getId();
        UserEntity userEntity = this.getById(id);
        String userPhone = userEntity.getPhone();
        String userEmail = userEntity.getQqEmail();
        String userPassword = userEntity.getPassword();
        // 如果修改了手机号，需要判断该手机号是否已绑定其它用户
        CompletableFuture<Void> phoneFuture = CompletableFuture.runAsync(() -> {
            String phone = userSaveWithAdminVO.getPhone();
            if (phone != null && !phone.equals(userPhone)) {
                Integer count = this.query().eq("phone", phone).count();
                if (count != null && count > 0) {
                    MyException.throwException("修改用户信息失败: 该手机号已绑定其它用户，请更换手机号");
                }
            }
        }, threadPoolExecutor);
        // 如果修改了QQ邮箱，需要判断该QQ邮箱是否已绑定其它用户
        CompletableFuture<Void> qqEmailFuture = CompletableFuture.runAsync(() -> {
            String qqEmail = userSaveWithAdminVO.getQqEmail();
            if (qqEmail != null && !qqEmail.equals(userEmail)) {
                Integer count = this.query().eq("qq_email", qqEmail).count();
                if (count != null && count > 0) {
                    MyException.throwException("修改用户信息失败: 该QQ邮箱已绑定其它用户，请更换QQ邮箱");
                }
            }
        }, threadPoolExecutor);
        // 如果修改了密码，需要进行MD5盐值加密
        CompletableFuture<Void> passwordFuture = CompletableFuture.runAsync(() -> {
            String password = userSaveWithAdminVO.getPassword();
            if (password != null && Md5Util.checkPassword(password, userPassword)) {
                userSaveWithAdminVO.setPassword(null);
            } else if (password != null) {
                userSaveWithAdminVO.setPassword(Md5Util.encryption(password));
            }
        }, threadPoolExecutor);
        // 等待所有异步任务都执行完，再更新用户信息
        CompletableFuture.allOf(phoneFuture, qqEmailFuture, passwordFuture).join();
        userEntity = BeanUtil.copyProperties(userSaveWithAdminVO, UserEntity.class);
        if (!this.updateById(userEntity)) {
            MyException.throwException("修改用户基本信息失败");
        }
        // 删除redis缓存
        redisCacheUtil.deleteByKey(CACHE_USER_BASE_KEY_PREFIX + id);
        // 发送消息到user.topic交换机，实现MySQL与ES的数据同步
        rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, id);
        return Result.ok("修改成功");
    }

    /**
     * 用户修改基本信息
     *
     * @param userUpdateVO 用户修改基本信息请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result updateUser(UserUpdateVO userUpdateVO) {
        Long id = userUpdateVO.getId();
        UserEntity userEntity = BeanUtil.copyProperties(userUpdateVO, UserEntity.class);
        if (!this.updateById(userEntity)) {
            MyException.throwException("用户信息修改失败");
        }
        // 删除redis缓存
        redisCacheUtil.deleteByKey(CACHE_USER_BASE_KEY_PREFIX + id);
        // 发送消息到user.topic交换机，实现MySQL与ES的数据同步
        rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, id);
        return Result.ok("修改成功");
    }

    /**
     * 发送短信验证码
     *
     * @param phone 手机号码
     * @return Result
     */
    @Override
    public Result sendSmsCaptcha(String phone) {
        if (RegexUtil.isPhoneInvalid(phone)) {
            return Result.error("手机号不合法，请输入合法的手机号");
        }
        // 防止1分钟内频繁发送验证码
        String captchaAndTime = stringRedisTemplate.opsForValue().get(CAPTCHA_PHONE_KEY_PREFIX + phone);
        if (captchaAndTime != null) {
            String[] split = captchaAndTime.split("_");
            long timeDifference = Long.parseLong(split[1]) + 60000 - System.currentTimeMillis();
            if (timeDifference > 0) {
                return Result.error("请勿频繁发送验证码，可以在" + timeDifference / 1000 + "秒后尝试发送验证码");
            }
        }
        SmsSendCaptchaDTO smsSendCaptchaDTO = new SmsSendCaptchaDTO();
        smsSendCaptchaDTO.setPhone(phone);
        String captcha = RandomUtil.randomNumbers(6);
        smsSendCaptchaDTO.setCaptcha(captcha);
        // 发送短信验证码
        Result result = thirdPartyFeign.sendSmsCaptcha(smsSendCaptchaDTO);
        if (result != null && HttpStatus.SC_OK == result.getCode()) {
            // 短信验证码发送成功，将 "验证码_当前时间" 加入redis缓存并设置有效期为5分钟
            stringRedisTemplate.opsForValue().set(CAPTCHA_PHONE_KEY_PREFIX + phone, captcha + "_" + System.currentTimeMillis(), CAPTCHA_PHONE_KEY_TTL, TimeUnit.MINUTES);
            return Result.ok("短信验证码发送成功，请注意及时查看");
        }
        return result;
    }

    /**
     * 校验手机号验证码是否正确
     *
     * @param phone   手机号
     * @param captcha 验证码
     * @return Result
     */
    @Override
    public Result checkPhoneCaptcha(String phone, String captcha) {
        if (RegexUtil.isPhoneInvalid(phone)) {
            MyException.throwException("手机号码格式错误");
        }
        if (RegexUtil.isCaptchaInvalid(captcha)) {
            MyException.throwException("验证码必须由6位数字组成");
        }
        String key = CAPTCHA_PHONE_KEY_PREFIX + phone;
        String captchaAndTime = stringRedisTemplate.opsForValue().get(key);
        if (captchaAndTime == null) {
            MyException.throwException("验证码已过期，请重新发送短信验证码");
        }
        String[] split = captchaAndTime.split("_");
        String correctCaptcha = split[0];
        if (!correctCaptcha.equals(captcha)) {
            MyException.throwException("验证码错误，请重新输入验证码");
        }
        return Result.ok("验证码正确");
    }

    /**
     * 用户更换绑定手机号
     *
     * @param updatePhoneVO 用户更换绑定手机号请求体
     * @return Result
     */
    @Override
    public Result updatePhone(UpdatePhoneVO updatePhoneVO) {
        Long id = updatePhoneVO.getId();
        String phone = updatePhoneVO.getPhone();
        // 1.校验验证码
        this.checkPhoneCaptcha(phone, updatePhoneVO.getCaptcha());
        // 2.校验该手机号是否已被其它用户绑定
        Integer count = this.query().eq("phone", phone).count();
        if (count > 0) {
            return Result.error("更换失败: 该手机号已被其它用户绑定，请更换其它手机号");
        }
        // 3.更换用户的手机号
        if (!this.update(new UpdateWrapper<UserEntity>()
                .set("phone", phone)
                .set("gmt_modified", LocalDateTime.now())
                .eq("id", id)
        )) {
            return Result.error("更换绑定手机号失败");
        }
        CompletableFuture.runAsync(() -> {
            // 4.删除redis缓存的用户信息
            redisCacheUtil.deleteByKey(CACHE_USER_BASE_KEY_PREFIX + id);
            // 5.发送消息到user.topic交换机，实现MySQL与ES的数据同步
            rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, id);
            // 6.验证码是一次性的，应从redis中删除验证码
            stringRedisTemplate.delete(CAPTCHA_PHONE_KEY_PREFIX + phone);
        }, threadPoolExecutor);
        return Result.ok("更换绑定手机号成功");
    }

    /**
     * 通过短信验证码修改密码
     *
     * @param updatePasswordVO 修改密码请求体
     * @return Result
     */
    @Override
    public Result updatePasswordByCaptcha(UpdatePasswordVO updatePasswordVO) {
        // 1.判断两次输入密码是否一致
        String newPassword = updatePasswordVO.getNewPassword();
        String checkPassword = updatePasswordVO.getCheckPassword();
        if (!newPassword.equals(checkPassword)) {
            return Result.error("两次密码输入不一致");
        }
        // 2.判断验证码是否正确
        String phone = updatePasswordVO.getPhone();
        this.checkPhoneCaptcha(phone, updatePasswordVO.getCaptcha());
        // 3.修改密码
        if (!this.update(new UpdateWrapper<UserEntity>()
                .set("password", Md5Util.encryption(newPassword))
                .set("gmt_modified", LocalDateTime.now())
                .eq("phone", phone)
        )) {
            return Result.error("密码修改失败: 该手机号对应的用户未注册");
        }
        // 4.验证码是一次性的，应从redis中删除验证码
        CompletableFuture.runAsync(() -> stringRedisTemplate.delete(CAPTCHA_PHONE_KEY_PREFIX + phone), threadPoolExecutor);
        return Result.ok("密码修改成功");
    }

    /**
     * 发送邮箱验证码
     *
     * @param qqEmail QQ邮箱
     * @return Result
     */
    @Override
    public Result sendEmailCaptcha(String qqEmail) {
        if (RegexUtil.isEmailInvalid(qqEmail)) {
            return Result.error("QQ邮箱不合法，请输入合法的QQ邮箱");
        }
        // 防止1分钟内频繁发送验证码
        String captchaAndTime = stringRedisTemplate.opsForValue().get(CAPTCHA_EMAIL_KEY_PREFIX + qqEmail);
        if (captchaAndTime != null) {
            String[] split = captchaAndTime.split("_");
            long timeDifference = Long.parseLong(split[1]) + 60000 - System.currentTimeMillis();
            if (timeDifference > 0) {
                return Result.error("请勿频繁发送验证码，可以在" + timeDifference / 1000 + "秒后尝试发送验证码");
            }
        }
        SimpleEmailDTO simpleEmailDTO = new SimpleEmailDTO();
        simpleEmailDTO.setQqEmail(qqEmail);
        simpleEmailDTO.setTitle("【失物招领平台】");
        String captcha = RandomUtil.randomNumbers(6);
        simpleEmailDTO.setText("您的验证码为：" + captcha + "，切勿将验证码泄露于他人，本条验证码有效期为5分钟。");
        // 发送邮箱验证码
        Result result = thirdPartyFeign.sendSimpleEmail(simpleEmailDTO);
        if (result != null && HttpStatus.SC_OK == result.getCode()) {
            // 短信验证码发送成功，将 "验证码_当前时间" 加入redis缓存并设置有效期为5分钟
            stringRedisTemplate.opsForValue().set(CAPTCHA_EMAIL_KEY_PREFIX + qqEmail, captcha + "_" + System.currentTimeMillis(), CAPTCHA_EMAIL_KEY_TTL, TimeUnit.MINUTES);
            return Result.ok("邮箱验证码发送成功，请注意及时查看");
        }
        return result;
    }

    /**
     * 校验邮箱验证码是否正确
     *
     * @param qqEmail QQ邮箱
     * @param captcha 验证码
     * @return Result
     */
    @Override
    public Result checkEmailCaptcha(String qqEmail, String captcha) {
        if (RegexUtil.isEmailInvalid(qqEmail)) {
            MyException.throwException("QQ邮箱不合法，请输入合法的QQ邮箱");
        }
        if (RegexUtil.isCaptchaInvalid(captcha)) {
            MyException.throwException("验证码必须由6位数字组成");
        }
        String key = CAPTCHA_EMAIL_KEY_PREFIX + qqEmail;
        String captchaAndTime = stringRedisTemplate.opsForValue().get(key);
        if (captchaAndTime == null) {
            MyException.throwException("验证码已过期，请重新发送短信验证码");
        }
        String[] split = captchaAndTime.split("_");
        String correctCaptcha = split[0];
        if (!correctCaptcha.equals(captcha)) {
            MyException.throwException("验证码错误，请重新输入验证码");
        }
        return Result.ok("验证码正确");
    }

    /**
     * 用户更换绑定邮箱
     *
     * @param updateEmailVO 用户更换绑定邮箱请求体
     * @return Result
     */
    @Override
    public Result updateEmail(UpdateEmailVO updateEmailVO) {
        Long id = updateEmailVO.getId();
        String qqEmail = updateEmailVO.getQqEmail();
        // 1.校验验证码
        this.checkEmailCaptcha(qqEmail, updateEmailVO.getCaptcha());
        // 2.校验该QQ邮箱是否已被其它用户绑定
        Integer count = this.query().eq("qq_email", qqEmail).count();
        if (count > 0) {
            return Result.error("更换失败: 该QQ邮箱已被其它用户绑定，请更换其它QQ邮箱");
        }
        // 3.更换用户的QQ邮箱
        if (!this.update(new UpdateWrapper<UserEntity>()
                .set("qq_email", qqEmail)
                .set("gmt_modified", LocalDateTime.now())
                .eq("id", id)
        )) {
            return Result.error("更换绑定邮箱失败");
        }
        CompletableFuture.runAsync(() -> {
            // 4.删除redis缓存的用户信息
            redisCacheUtil.deleteByKey(CACHE_USER_BASE_KEY_PREFIX + id);
            // 5.发送消息到user.topic交换机，实现MySQL与ES的数据同步
            rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, id);
            // 6.验证码是一次性的，应从redis中删除验证码
            stringRedisTemplate.delete(CAPTCHA_EMAIL_KEY_PREFIX + qqEmail);
        }, threadPoolExecutor);
        return Result.ok("更换绑定邮箱成功");
    }

    /**
     * 完成用户登录
     *
     * @param userDetailInfoVO 用户详细信息
     * @return Result
     */
    private Result doLogin(UserDetailInfoVO userDetailInfoVO) {
        Long userId = userDetailInfoVO.getId();
        StpUtil.login(userId, new SaLoginModel()
                // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型
                .setDevice("PC")
                // 记住我模式: 是否为持久Cookie（临时Cookie在浏览器关闭时会自动删除，持久Cookie在重新打开后依然存在）
                .setIsLastingCookie(true)
        );
        // 获取当前登录用户的角色与权限信息
        RoleAndPermissionDTO roleAndPermissionDTO = this.queryRoleAndPermissionById(userId);
        // 获取当前登录用户的token信息
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        // 返回用户详情信息和token信息
        Result result = Result.ok("登录成功");
        result.put("tokenInfo", tokenInfo);
        result.put("userDetailInfoVO", userDetailInfoVO);
        result.put("roleAndPermissionDTO", roleAndPermissionDTO);
        return result;
    }

    /**
     * 插入默认用户信息
     *
     * @param phone    手机号
     * @param password 登录密码
     * @param roleCode 角色码
     * @return UserDetailInfoVO
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public UserDetailInfoVO insertDefaultUser(String phone, String password, String roleCode) {
        // 1.将用户基本信息插入数据库
        UserEntity user = new UserEntity();
        user.setPhone(phone);
        user.setPassword(password != null ? Md5Util.encryption(password) : Md5Util.encryption(RandomUtil.randomNumbers(10)));
        user.setNickname("user_" + RandomUtil.randomNumbers(10));
        user.setImgUrl(DEFAULT_IMG_URL);
        this.save(user);
        // 2.发送消息到user.topic交换机，实现MySQL与ES的数据同步
        CompletableFuture.runAsync(() -> rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, user.getId()), threadPoolExecutor);
        // 3.给该用户分配的角色信息
        CompletableFuture.runAsync(() -> {
            RoleEntity role = roleService.query().eq("role_code", roleCode).one();
            userRoleService.insertUserRole(new UserRoleSaveVO(user.getId(), role.getId()));
        }, threadPoolExecutor);
        // 4.返回用户信息
        return BeanUtil.copyProperties(user, UserDetailInfoVO.class);
    }

    /**
     * 短信验证码注册
     *
     * @param phoneCaptchaRegisterVO 短信验证码注册请求体
     * @return Result
     */
    @Override
    public Result phoneCaptchaRegister(PhoneCaptchaRegisterVO phoneCaptchaRegisterVO) {
        // 1.判断两次输入密码是否一致
        String newPassword = phoneCaptchaRegisterVO.getPassword();
        String checkPassword = phoneCaptchaRegisterVO.getCheckPassword();
        if (!newPassword.equals(checkPassword)) {
            return Result.error("两次密码输入不一致");
        }
        // 2.校验验证码
        String phone = phoneCaptchaRegisterVO.getPhone();
        this.checkPhoneCaptcha(phone, phoneCaptchaRegisterVO.getCaptcha());
        // 3.判断该手机号是否已绑定其它用户
        Integer count = this.query().eq("phone", phone).count();
        if (count != null && count > 0) {
            return Result.error("该手机号已绑定其它用户，请更换手机号");
        }
        // 4.将用户数据插入到数据库
        UserDetailInfoVO userDetailInfoVO = currentProxy.insertDefaultUser(phone, newPassword, "user");
        // 5.验证码是一次性的，应从redis中删除验证码
        CompletableFuture.runAsync(() -> stringRedisTemplate.delete(CAPTCHA_PHONE_KEY_PREFIX + phone), threadPoolExecutor);
        // 6.完成登录
        return this.doLogin(userDetailInfoVO);
    }

    /**
     * 短信验证码登录
     *
     * @param phoneCaptchaLoginVO 短信验证码登录请求体
     * @return Result
     */
    @Override
    public Result phoneCaptchaLogin(PhoneCaptchaLoginVO phoneCaptchaLoginVO) {
        // 1.校验验证码
        String phone = phoneCaptchaLoginVO.getPhone();
        this.checkPhoneCaptcha(phone, phoneCaptchaLoginVO.getCaptcha());
        // 2.查询用户是否已注册
        UserEntity userEntity = this.query().eq("phone", phone).one();
        UserDetailInfoVO userDetailInfoVO;
        if (userEntity == null) {
            // 3.该用户未注册，插入该用户信息到数据库
            userDetailInfoVO = currentProxy.insertDefaultUser(phone, null, "user");
        } else {
            // 4.该用户已经注册，校验用户是否被封号
            if (StpUtil.isDisable(userEntity.getId())) {
                // 获取指定账号剩余封禁时间，单位：秒，如果该账号未被封禁，则返回-2
                long disableTime = StpUtil.getDisableTime(userEntity.getId());
                if (disableTime == -1) {
                    return Result.error("登录失败，您的账号已被永久封禁");
                }
                return Result.error("登录失败，您的账号已被封禁，将于" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis() + disableTime * 1000) + "进行解封");
            }
            userDetailInfoVO = BeanUtil.copyProperties(userEntity, UserDetailInfoVO.class);
        }
        // 5.验证码是一次性的，应从redis中删除验证码
        CompletableFuture.runAsync(() -> stringRedisTemplate.delete(CAPTCHA_PHONE_KEY_PREFIX + phone), threadPoolExecutor);
        // 6.完成用户登录
        return doLogin(userDetailInfoVO);
    }

    /**
     * 手机号密码登录
     *
     * @param phonePasswordLoginVO 手机号密码登录请求体
     * @return Result
     */
    @Override
    public Result phonePasswordLogin(PhonePasswordLoginVO phonePasswordLoginVO) {
        // 1.查询用户信息
        UserEntity userEntity = this.query().eq("phone", phonePasswordLoginVO.getPhone()).one();
        // 2.判断用户名是否存在
        if (userEntity == null) {
            return Result.error("用户名不存在");
        }
        // 3.判断登录密码是否正确
        if (!Md5Util.checkPassword(phonePasswordLoginVO.getPassword(), userEntity.getPassword())) {
            return Result.error("登录密码错误");
        }
        // 5.该用户已经注册，校验用户是否被封号
        if (StpUtil.isDisable(userEntity.getId())) {
            // 获取指定账号剩余封禁时间，单位：秒，如果该账号未被封禁，则返回-2
            long disableTime = StpUtil.getDisableTime(userEntity.getId());
            if (disableTime == -1) {
                return Result.error("登录失败，您的账号已被永久封禁");
            }
            return Result.error("登录失败，您的账号已被封禁，将于" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis() + disableTime * 1000) + "进行解封");
        }
        // 6.完成登录
        return doLogin(BeanUtil.copyProperties(userEntity, UserDetailInfoVO.class));
    }

    /**
     * 账号封禁
     *
     * @param userDisableVO 账号封禁/解封请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result disable(UserDisableVO userDisableVO) {
        Long userId = userDisableVO.getUserId();
        String qqEmail = userDisableVO.getQqEmail();
        String reason = userDisableVO.getReason();
        Long time = userDisableVO.getTime();
        if (time == null) {
            return Result.error("封禁失败: 封号截止时间戳不能为空");
        }
        // 永久封禁
        if (time.equals(-1L)) {
            // 先将用户强制下线
            StpUtil.logout(userId);
            // 设置用户的封号时长
            StpUtil.disable(userId, time);
            if (qqEmail != null) {
                CompletableFuture.runAsync(() -> {
                    SimpleEmailDTO simpleEmailDTO = new SimpleEmailDTO();
                    simpleEmailDTO.setQqEmail(qqEmail);
                    simpleEmailDTO.setTitle("【失物招领平台】---账号禁用");
                    simpleEmailDTO.setText("您的账号于" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "被永久禁用！\n" +
                            "封号原因: " + reason);
                    // 发送邮件
                    thirdPartyFeign.sendSimpleEmail(simpleEmailDTO);
                }, threadPoolExecutor);
            }
        } else if (time <= System.currentTimeMillis()) {
            return Result.error("封号截止时间不能在当前时间之前");
        } else {
            // 先将用户强制下线
            StpUtil.logout(userId);
            // 设置用户的封号时长
            StpUtil.disable(userId, (time - System.currentTimeMillis()) / 1000);
            if (qqEmail != null) {
                CompletableFuture.runAsync(() -> {
                    SimpleEmailDTO simpleEmailDTO = new SimpleEmailDTO();
                    simpleEmailDTO.setQqEmail(qqEmail);
                    simpleEmailDTO.setTitle("【失物招领平台】---账号禁用");
                    simpleEmailDTO.setText("您的账号于" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "被禁用，\n" +
                            "将于" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time) + "进行解封，\n" +
                            "封号原因: " + (StrUtil.isBlank(reason) ? "无" : reason));
                    // 发送邮件
                    thirdPartyFeign.sendSimpleEmail(simpleEmailDTO);
                }, threadPoolExecutor);
            }
        }
        // 修改用户的封号状态为: 已封号
        this.updateById(UserEntity.builder()
                .id(userId)
                .disableStatus(AuthConstant.UserDisableEnum.DISABLE.getCode())
                .build()
        );
        // 删除redis缓存
        redisCacheUtil.deleteByKey(CACHE_USER_BASE_KEY_PREFIX + userId);
        // 发送消息到user.topic交换机，实现MySQL与ES的数据同步
        rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, userId);
        return Result.ok("账号封禁成功");
    }

    /**
     * 账号解封
     *
     * @param userDisableVO 账号封禁/解封请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result untieDisable(UserDisableVO userDisableVO) {
        Long userId = userDisableVO.getUserId();
        String qqEmail = userDisableVO.getQqEmail();
        // 判断指定账号是否已被禁用
        if (!StpUtil.isDisable(userId)) {
            return Result.error("该账号处于未封禁状态，请勿重复该操作");
        }
        // 解除封禁
        StpUtil.untieDisable(userId);
        // 修改用户的封号状态为: 已封号
        this.updateById(UserEntity.builder()
                .id(userId)
                .disableStatus(AuthConstant.UserDisableEnum.ENABLE.getCode())
                .build()
        );
        // 删除redis缓存
        redisCacheUtil.deleteByKey(CACHE_USER_BASE_KEY_PREFIX + userId);
        // 发送消息到user.topic交换机，实现MySQL与ES的数据同步
        rabbitTemplate.convertAndSend(USER_EXCHANGE, USER_SAVE_KEY, userId);
        if (qqEmail != null) {
            CompletableFuture.runAsync(() -> {
                SimpleEmailDTO simpleEmailDTO = new SimpleEmailDTO();
                simpleEmailDTO.setQqEmail(qqEmail);
                simpleEmailDTO.setTitle("【失物招领平台】---账号解封");
                simpleEmailDTO.setText("您的账号于" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "被解封！");
                // 发送邮件
                thirdPartyFeign.sendSimpleEmail(simpleEmailDTO);
            }, threadPoolExecutor);
        }
        return Result.ok("账号解封成功");
    }

}